Triton

1.1. Что такое Triton?

Triton Inference Server — это набор программного обеспечения от NVIDIA с открытым исходным кодом, которое организует процесс работы моделей машинного обучения, а также любых других процессов, описанных одним из допустимых способов. Triton Inference Server (далее Triton или Тритон) позволяет организовывать модели различным образом, размещая их независимо или объединяя в сложные структурные последовательности, некие конвейеры обработки данных.

Почему не отдельный сервис под каждое интеллектуальное решение?

Для разработки сервиса требуются дополнительные люди, время, технологии, отличные от тех, с которыми обычно работает дата-саентист. Triton же предоставляет определённый формат для моделей, конфигурационные файлы для их настройки, а также общий принцип для работы с моделями.

В случае подхода "одно решение — один отдельный сервис" подразумевается обычно, что логика решения тесно переплетается с сервисом: что-то убрано в переносимые части, что-то встроено в сам сервис. При необходимости внесения корректировок только в логику решения нужно взаимодействовать еще и с сервисной реализацией. Такое взаимодействие сильно увеличивает путь решения и повышает риски что-то сломать, поскольку каждый сервис в таком случает обладает высокой уникальностью.

Triton — это решение из коробки, которое настраивается и запускается один раз, а все модели размещаются внутри по одинаковому сценарию.

1.2. Потребности, преимущества, недостатки

Немного предыстории

Новое решение вводится либо когда до него просто ничего нет, либо когда старое решение совсем никуда не годится.

В нашем случае был как раз второй вариант. До появления тритона, придерживались схемы "одно решение — один сервис", который его в полной мере содержит, предоставляя к нему доступ. Разработка сервиса занимала много времени, разработка ML-решения занимала тоже много времени. Когда обе части становились достаточно готовыми, начинался процесс общей отладки и тестирования. И он занимал порой еще больше времени, чем предыдущие этапы, поскольку составные части не подходили друг к другу, типы данных отличались, каждый раз настраивались процессы загрузки весов и иных артефактов. К слову, вся интеграция была каждый раз уникальной и не имела стандартизации. Именно тогда появилось желание отказаться от существующего подхода и искать новый.

Потребности

  1. Постоянная доступность — вызвана необходимостью держать все разрабатываемые решения в режиме онлайн доступа для клиентов.
  2. Высокие нагрузки — спрос на решения рос и растёт до сих пор, поэтому не принимать этот пункт нельзя.
  3. Сокращение времени на разработку сервисов, предоставляющих доступ к решениям.
  4. Стандартизация подходов к разработке и внедрению новых решений.
  5. Удобное изменение решения без влияния на сервис доступа.
  6. Возможность использовать весь арсенал ML и DL библиотек и фреймворков, без дополнительных затрат на совместимость.
  7. Использование графических процессоров (GPU) для вычисления.

На первый взгляд кажется, что отличным решением мог бы быть свой продукт. Но делать свой продукт с нуля — очень-очень долго, во-первых. Во-вторых, его надо поддерживать. В-третьих, долго искать людей, которые захотят с ним работать.

Запросы серьезные, поэтому вариантов тут не так уж и много: нужно найти готовое решение и принять его за стандарт. К нашему удивлению готовое решение было одно, правда на тот момент весьма сырое. Этим решением оказался Тритон с актуальными даже данный момент преимуществами.

Преимущества

  1. Тритон обеспечивает работу в режиме oнлайн с высокими нагрузками.
  2. Достаточно легко разворачивается на любой машине.
  3. Предоставляет постоянный интерфейс доступа к любому развернутому внутри решению.
  4. Поддерживает вычисления как на различных CPU, так и на GPU, но только от NVIDIA.
  5. Предоставляет средства настройки и оптимизации решений.
  6. Фиксирует некоторый стандарт интерфейса для составляющих решение моделей.
  7. Поддерживает большое количество инструментов для разработки моделей.
  8. Позволяет объединять модели в сложные системы в виде ансамблей.
  9. Развивается и имеет поддержку, непосредственно производителем GPU.
  10. Имеет официальную документацию.

Преимуществ много, и они весьма очевидны. Нашлись и некоторые недостатки, о которых стоит упомянуть.

Недостатки

  1. Официальная документация оставляет желать лучшего.
  2. Несмотря на растущее комьюнити, в сети всё еще достаточно мало примеров его использования.
  3. На данный момент не предоставляет возможности организовать распределённые вычисления на разных машинах.
  4. В какой-то мере не самое оптимальное использования графического процессора и памяти.

Учитывая, что альтернативы особой нет, а недостатки весьма изысканные, конечно же выбор остановился на Тритоне.

Кстати говоря, курс который сейчас перед тобой, дорогой читатель, появился как раз благодаря самому первому недостатку. Так что для кого-то это может быть и преимуществом =)

1.3. Архитектура

Общие сведения об архитектуре

Создавая сервисы, мы придерживаемся микросервисного подхода, который подразумевает достаточно сильную независимость составляющих компонент. Но хотя микросервисная архитектура и допускает абсолютно разные языки программирования, большая часть сервисов у нас пишется на компилируемом языке Go, что как минимум позволяет сохранять универсальность backend-разработчиков.

Ещё раз обсудим, что такое Тритон. Тритон — это независимый сервис, который организует вычисления размещаемых внутри него моделей. Далее в курсе мы обсудим, что модели могут быть реализованы с помощью разных языков или фреймворков.

В итоге мы получаем, что все манипуляции связанные с межсервисным взаимодействием описываются с использованием фиксированного стека, а дата-саентистам остается свобода в выборе инструментов для возможности интегрировать почти любое решение. 

Pasted image 20250523192400.png

На рисунке 1 наглядно показано, что Тритон выполняет разграничительную функцию между backend-разработчиками и дата-саентистами. Тем самым снимая с первых сложную задачу в виде необходимости разбираться в том, что делают вторые, для обслуживания своих сервисов.

Pasted image 20250523192418.png

На рисунке 2 продемонстрирована более детальная структура в пределах одной машины, на которой разворачивается Тритон. Он выступает управляющим элементом, который принимает входной трафик и команды, распределяет их по соответствующим моделям в определённом порядке, собирает ответы моделей и возвращает результат назад клиенту. Тритон, кроме того, хорошо взаимодействует с аппаратной частью, предоставляя различные режимы запуска моделей и потребления ресурсов.

2.1. Что называем сервером

Что называем сервером?

В нынешней парадигме Тритон представляет собой сервис, который внешне ничем не отличается от многих других сервисов. Он ровно также располагается на одном физическом сервере или на нескольких физических серверах отдельными экземплярами под управлением внешнего оркестратора.

Так при чём тут тогда сервер?

Под сервером мы будем подразумевать постоянно запущенный сервис Тритона, который обслуживает загружаемые нами модели и управляет всеми остальными процессами, относящимися к ним.

Под бэкендом будет подразумеваться поддерживаемая Тритоном платформа для реализации модели.

Как устроен сервер

Сам сервис и основной набор функций тритона написан на С++. Некоторая часть функций имеет обёртку на языке python. Сервис по умолчанию имеет два порта для обращения по GRPC и по HTTP API, а также ещё один с HTTP интерфейсом для сбора метрик утилизации GPU, CPU, памяти и других.

Как уже было сказано, в серверной части располагаются все модели, которые предполагается запускать. Хранятся они в локальной директории называемой репозиторием. При запуске путь до этой директории явно передаётся Тритону в виде параметра. При дальнейшей работе, чтобы загрузить новую модель в Тритон, она должна быть помещена как раз в этот репозиторий.

Типизация данных

Поскольку модели могут быть написаны с использованием разных фреймворков и языков (бэкендов), появилась необходимость в сопоставлении типизации данных этих языков с языком ядра. Для простоты использования типы данных явно указываются в конфигурационном файле каждой модели в зависимости от назначений её входов и выходов, а также от выбранного инструмента реализации в соответствии со следующей далее таблицей.

Pasted image 20250523192445.png

В таблице 1 указано соответствие типов данных, актуальную таблицу можно найти на официальном сайте.

  • В первом столбце находится название типа данных, которое указывается файле конфигурации модели.
  • В следующих четырех столбцах показано соответствие типов данных для поддерживаемых платформ моделей. Пропуски в таблице означают отсутствие поддержки Тритоном указанного типа.
  • В столбце «API» показан тип данных языка C++, протоколов HTTP/REST и GRPC.
  • В последнем столбце показан соответствующий тип данных для Python библиотеки NumPy.

2.2. Как поднять сервер

Как поднять сервер?

Чуть ранее была отмечена простота развёртывания Тритона. И действительно, разработчики решения о многом уже позаботились. Чтобы запустить свой Тритон сервер нужно спулить готовый образ или взять подходящий Docker-файл из официального репозитория и собрать образ самостоятельно. Мы остановимся на первом варианте.

Из установленного программного обеспечения нам потребуется только Docker.

Note

Берём из официального источника название актуального образа. На данный момент это nvcr.io/nvidia/tritonserver:24.05-py3, соответствующий версии Тритона 2.46.0. Другие соответствия версий можно посмотреть здесь.

Далее пишем небольшую инструкцию для запуска в виде docker-compose.yml

version: '3.3'
services:
	tritonserver:
		ports:
			- '19090:8000'
			- '19091:8001'
			- '19092:8002'
		volumes:
			- '/LOCAL_PATH_TO_MODELS:/models'
		image: nvcr.io/nvidia/tritonserver: 24.05-py3
		command: tail -F anything
		deploy:
			resources:
				reservations:
					devices:
						- driver: nvidia
							device_ids: [ '5' ]
							capabilities: [ gpu ]
		shm_size: '128gb'

Подробнее разберём, что происходит в нашей инструкции.

  1. Мы определяем сервис с названием tritonserver.
  2. Делаем маппинг между портами Triton (8000, 8001, 8002) и свободными локальными портами. Как уже говорилось их 3: первый для GRPC запросов, второй за HTTP, третий для получения метрик, характеризующих работу Triton.
  3. Монтируем внешнюю директорию LOCAL_PATH_TO_MODELS, в которой лежат наши модели, в локальную директорию /models в контейнере.
  4. Если для моделей нужны видеокарты, то пропишем их (в нашем случае укажем 5-ю в device_ids).

Переходим в директорию с docker-compose.yml и поднимаем контейнер с Тритоном с помощью команды docker-compose up -d.
После поднятия контейнера заходим в него: docker exec -it N bash,
где N это id контейнера. И поднимаем Triton-сервер следующей командой: tritonserver --model-repository=/models.
Сервер поднят и готов к работе!

3.1. Модели

Модель — это элементарная часть, которая может нести определённую логику обработки данных. Моделей в тритоне может быть очень много. Их количество ограничено только аппаратными ресурсами.

Находятся они, как уже было сказано ранее в репозитории (Model Repository) — хранилище на основе файловой системы, имеющей следующий вид:

Pasted image 20250523193902.png

Здесь model-repository-path — это каталог в файловой системе, в который складываются модели и о котором знает Тритон сервер.

Рассмотрим общую структуру любой модели подробнее. Каждая модель лежит в каталоге с совпадающем с её названием. В данном случае это model_name. Внутри каждого каталога абсолютно любой модели лежит конфигурационный файл config.pbtxt, который содержит информацию для Тритона о том, какая это модель.

Рядом с этим конфигурационным файлом находится каталог с названием в виде натурального числа. Это число отображает версию модели. Именно на него ориентируется тритон, заходя регулярно в директорию и проверяя изменения: если версия изменилась — нужно загрузить из репозитория свежую модель, если нет —  ничего делать не нужно. Внутри этого каталога лежит модель, реализованная на поддерживаемом бэкенде.

model-repository-path
  ├── model_name
      ├── config.pbtxt
      ├── 1
      │   └── model
      └── ...

Рассмотрим подробнее config.pbtxt

name: "model_name"
backend: "python"

input [
  {
    name: "TEXT"
    data_type: TYPE_STRING
    dims: [ 1 ]
  },
  ...
]

output [
  {
    name: "IS_MATCH"
    data_type: TYPE_BOOL
    dims: [ 1 ]
  },
  ...
]

instance_group [
  {
    count: 4
    kind: KIND_CPU
  }
]

parameters: [
  {
    key: "EXECUTION_ENV_PATH",
    value: {string_value: "$$TRITON_MODEL_DIRECTORY/conda_env.tar.gz"}
  }
]

На первой строчке указывается название модели, ровно такое же как название содержащего конфиг каталога.

Далее указывается тип бэкенда, на котором реализована модель, в данном случае указан python-бэкенд.

В списках input и output описываются названия входных и выходных значений модели соответсвенно. У каждого указывается название, тип в соответствии с таблицей, рассмотренной ранее, и размерность.  [1] - соответствует одному значению, [ 2 ] -  вектору длиной 2,  [ 3, 3 ] -  матрице 3 на 3 и т.д.

В разделе instance_group указывается, на чем конкретно будет запущена модель: CPU/GPU, а также количество экземпляров модели, которое будет поднято. В данном конкретном примере модель будет поднята на ЦПУ с 4 инстансами.

В разделе parameters указываются дополнительные key-value параметры характерные каждому отдельному случаю. В этом примере указывается путь до окружения, в котором будет запущена модель, реализованная на python-бекенде.

3.2. Ансамбли

В предыдущем разделе было дано определение модели, в котором упоминалось, что модель — это именно элементарная составляющая. Настало время рассказать, составляющими чего могут являться модели.

Ансамбль — это система построенная из одной и более моделей (пайплайн), представленная в виде направленного графа без циклов. Представлять решение в виде ансамбля следует, когда не удаётся всю логику собрать на одном бэкенде или появляются узкие места, избавиться от которых можно только с помощью увеличения числа экземпляров отдельных моделей.

Например, этапы data preprocessing -> model inference -> data postprocessing редко удаётся собрать в виде одной модели и приходится собирать ансамбль из 3х моделей.

Сам по себе ансамбль — это просто спецификация (конфигурационный файл), определяющая порядок передачи данных между моделями с консистентными входами.

Рассмотрим на конкретном примере структуру и конфигурацию ансамбля.

Структура:

├── triton_models
│   ├── ensemble
│   │   ├── config.pbtxt
│   │   └── 1
│   ├── dali
│   │   ├── config.pbtxt
│   │   └── 1
│   │       └── model.dali
│   └── model
│       ├── config.pbtxt
│       └── 1
│           └── model.plan

Видим, что в репозитории triton_models лежит 2 модели и одно описание ансамбля. Отличительной чертой в содержимом директорий ансамбля и модели является то, что у ансамбля каталог с версией модели пустой.

В данном случае в ансамбле используется два последовательных шага:

  1. dali - этап с декодированием изображения из байтового представления, по умолчанию Тритон обращается к модели с названием model.dali;
  2. model - этап с инференсом модели на бэкенде TensorRT, по умолчанию Тритон обращается к модели с названием model.plan

Конфигурация ансамбля:

name: "ensemble"
platform: "ensemble"
max_batch_size: 4

input [
  {
    name: "IMAGE"
    data_type: TYPE_UINT8
    dims: [ -1 ]
  }
]

output [
  {
    name: "LOGITS"
    data_type: TYPE_STRING
    dims: [ 1 ]
  }
]

ensemble_scheduling {
  step [
    {
      model_name: "dali"
      model_version: -1
      input_map {
        key: "DALI_INPUT_0"
        value: "IMAGE"
      }
      output_map {
        key: "DALI_OUTPUT_0"
        value: "dali_image"
      }
    },
    {
      model_name: "model"
      model_version: -1
      input_map {
        key: "input"
        value: "dali_image"
      },
      {
	  model_name: "model"
	  model_version: -1
	  input_map {
	    key: "input"
	    value: "dali_image"
	  }
	  output_map {
	    key: "LOGITS"
	    value: "LOGITS"
	  }
    }
  ]
}

В данном случае используется динамический батчинг с максимальным размером батча 16 — это опциональная вещь и можно всегда собирать статический батч, убрав dynamic_batching из конфига.

preffered_batch_size — размерности батча, который пытается сформировать тритон перед инференсом модели. Их стоит подбирать руками или использовать методы профилирования. В нашем случае сервис, посылающий запросы в тритон, отправляет батчи размера 4. Поэтому здесь подобраны именно такие значения для максимальной пропускной способности при большой нагрузке. В других случаях может быть неэффективно использовать большой список предпочитаемых значений, особенно таких маленьких как 2.

max_queue_delay_microseconds— если запросы поступают с достаточной частотой, и тритон может сформировать батч с preferred_batch_size, тогда инференс выполняется незамедлительно, иначе сервер будет ожидать новые порции данных для формирования батча в течение max_queue_delay_microseconds.

4.1. Python backend

Python бэкенд

Без преувеличения можно сказать, что это самый универсальный бэкенд для реализации некоторой логики внутри модели. Обращаем особое внимание, что в силу этой же универсальности он далеко не самый быстрый и оптимальный.

Формат модели на Python

Для python-бэкенда необходимо наличие файла model.py. В нём описывается стандартный интерфейс модели в виде класса TritonPythonModel. Кроме того, рядом с _model.py, могут лежать другие необходимые файлы и каталоги с кодом или артефактами в виде pth, yaml, и т.д.

Рассмотрим подробнее класс TritonPythonModel.

Обязательные требования к этому классу:

1. название — оно должно быть всегда таким;
2. наличие метода initialize — вызывается на этапе загрузки модели Тритоном;
3. наличие метода execute — вызывается, когда в Тритон пришёл запрос именно к этой модели;
4. наличие метода finalize — вызывается на этапе выгрузки модели Тритоном.

Рассмотрим далее следующий пример.

import json
import triton_python_backend_utils as pb_utils

class TritonPythonModel:
    def initialize(self, args):
        self.model_config = json.loads(args["model_config"])

        output0_config = pb_utils.get_output_config_by_name(model_config, "OUTPUT0")
        output1_config = pb_utils.get_output_config_by_name(model_config, "OUTPUT1")

        self.output0_dtype = pb_utils.triton_string_to_numpy(
            output0_config["data_type"]
        )

        self.output1_dtype = pb_utils.triton_string_to_numpy(
            output1_config["data_type"]
        )

	def execute(self, requests):
	    responses = []

	    for request in requests:
	        in_0 = pb_utils.get_input_tensor_by_name(request, "INPUT0")
	        in_1 = pb_utils.get_input_tensor_by_name(request, "INPUT1")
	
	        out_0, out_1 = (
	            in_0.as_numpy() + in_1.as_numpy(),
	            in_0.as_numpy() - in_1.as_numpy(),
	        )
	
	        out_tensor_0 = pb_utils.Tensor("OUTPUT0", out_0.astype(self.output0_dtype))
	        out_tensor_1 = pb_utils.Tensor("OUTPUT1", out_1.astype(self.output1_dtype))
	
	        inference_response = pb_utils.InferenceResponse(
	            output_tensors=[out_tensor_0, out_tensor_1]
	        )
	
	        responses.append(inference_response)
	
	    return responses

	def finalize(self): 
		print("Модель успешно выгрузилась!")
initialize

В initialize мы подтягиваем выходные типы из конфигурационного файла config.pbtxt, они в процессе работы не меняются поэтому сделать это можно однократно на этапе загрузки. Также на этом этапе допустимо загружать веса модели, создавать её экземпляр и инициализировать другие переменные, чтобы сократить время выполнения метода execute.

execute

У нас методе execute лишь моделируется некоторая логика, которая будет выполняться, когда модель придут одиночные запросы или батчи. Важно иметь в виду, что этот метод каждый раз выполняется, когда приходит запрос. Именно вычислительная сложность этого метода будет влиять на скорость отклика модели. 

finalize

В эпоху существования сборщиков мусора метод finalize немного потерял свой основной вес, но тем не менее по-прежнему существует ряд задач, которые может потребоваться решать перед выгрузкой задач, например, логирование момента выгрузки или состояний.

Окружения

В контейнере Triton для работы с моделями, реализованными на Python, присутствует python-интерпретатор. Версия этого интерпретатора зависит от версии серверной части Triton и сборки самого контейнера.

Безусловно, можно использовать базовый интерпретатор, но ровно до тех пор, пока не появится потребность в разных наборах библиотек для разных моделей на python-бэкенде.

Часто бывает необходимо использовать библиотеку или фреймворк, которых нет на уже запущенном сервере. Перезапускать production сервер для установки дополнительных пакетов — не оправдано, как минимум, из-за высокого риска конфликта версий библиотек. Также библиотеки часто могут не поддерживать работу с текущей версией интерпретатора. Для исключения подобных проблем Triton-сервер имеет поддержку виртуальных окружений (virtualenv, conda environment).

models
├── model_1
│   ├── 1
│   │   ├── model.py
│   ├── config.pbtxt
│   └── conda_env.tar.gz

Под каждую модель, написанную на Python отдельно или в составе ансамбля, рекомендуется:

1️. Создать своё окружение, установив в него необходимые для модели пакеты;
2️. Разместить его в каталоге с названием модели рядом с файлом config.pbtxt
3️. Указать название архива с окружением в файле config.pbtxt.

Config.pbtxt
В config.pbtxt файле требуется указать в качестве backend значение "python", а в дополнительных параметрах — путь до окружения, если оно имеется.

backend: "python"
...
parameters: [{
  key: "EXECUTION_ENV_PATH",
  value: {
    string_value: "$$TRITON_MODEL_DIRECTORY/conda_env.tar.gz"
  }
}]
Пример 1

В репозитории production сервера уже загружены и работают две модели. Необходимо развернуть третью модель, которой нужны дополнительные пакеты. Пересборка образа не допустима, т.к. она влечёт существенные затраты по времени.

Пример 2

Для новой модели устанавливается набор библиотек в среде python 3.9. Серверная python-оболочка скомпилирована с использованием Python 3.10. Получается, что этот набор библиотек не будет доступен в новой модели.

models
├── model_1
│   ├── 1
│   │   ├── model.py
│   ├── config.pbtxt
│   ├── conda_env_3.9.tar.gz
│   └── triton_python_backend_stub

Cборка переносимого окружения

В качестве постоянных элементов потребуется  conda, дополнительный пакет conda-pack, для упаковки собранного окружения, и установщик пакетов pip по необходимости.

export CONDA_SOURCE=https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
export CONDA_INSTALLER=Miniconda3.sh
export SAVE_FOLDER=/tmp/conda_env
export ENV_NAME=conda_env_3_9
export PYTHON_VERSION=3.9

wget $CONDA_SOURCE -O $SAVE_FOLDER/$CONDA_INSTALLER
chmod +x $SAVE_FOLDER/$CONDA_INSTALLER
bash $CONDA_INSTALLER -b -p $SAVE_FOLDER/miniconda
conda init bash

conda create -ny $ENV_NAME python=$PYTHON_VERSION
conda activate $ENV_NAME
conda install -y conda-pack pip
...
conda pack -fo $SAVE_FOLDER/$ENV_NAME.tar.gz
...
conda deactivate
rm -r $SAVE_FOLDER

Bash-скрипт для сборки не представляет чего-то особо сложного, но всё-таки давайте по нему пробежимся.
В самом начале мы определяем в переменных путь откуда будем качать скрипт установки конды, локальное название этого скрипта, временный каталог для манипуляций, название окружения и версию python.
Далее скачиваем скрипт, запускаем его, указав путь установки, затем инициализируем конду.
Создаем новое окружение, ставим в него необходимые для модели зависимости через pip или conda и упаковываем окружение в архив .tar.gz.
Забираем архив с окружением, а затем выносим за собой мусор.

В случаях, когда версия интерпретатора в переносимом окружении отличается от версии интерпретатора triton-сервера, для исправной работы модели потребуется скомпилировать новый файл-заглушку triton_python_backend_stub. Этот файл является связующим звеном между интерпретатором переносимого окружения и скомпилированным ядром Triton, написанном на C++.

О сборке triton_python_backend_stub можно прочитать в официальной документации.

4.2. Other backends

Другие бэкенды

Тритон заточен под использование моделей в реальном времени, что зачастую накладывает определённые требования к скорости инференса модели. Если базовый движок на python-бэкенде (pytorch/tensorflow) не может дать необходимую скорость работы, то могут помочь различные оптимизации, заточенные под быстрый инференс. В этом разделе мы затронем ONNX, Tensorrt, Treelite и OpenVINO.

ONNX

ONNX (Open Neural Network Exchange) представляет собой формат с открытым исходным кодом, разработанный для облегчения взаимодействия между различными фреймворками глубокого обучения. Основной особенностью ONNX является его способность служить мостом между различными фреймворками глубокого обучения, такими как PyTorch, TensorFlow, Caffe, и другие.

ONNX представляет модели в виде направленного ациклического графа (DAG), где операции с данными выступают узлами, а рёбра представляют поток данных между этими операциями. Эта графовая модель является эффективной и гибкой, отражая сущность различных архитектур нейронных сетей. Формат поддерживает широкий спектр операторов нейронных сетей, что делает его подходящим для моделирования различных задач, от классификации изображений и обработки естественного языка до обучения с подкреплением. Если вы хотите перенести модель из свежей статьи из arxiv.org, или использовать какую-то экзотическую архитектуру, то ONNX поддерживает написание кастомных слоев.

Для конвертации модели в ONNX можно воспользоваться встроенным функционалом фреймворка, в котором проходило обучение, или воспользоваться одним из конвертеров из документации ONNX https://github.com/onnx/tutorials#converting-to-onnx-format

TensorRT

TensorRT — библиотека, разработанная компанией NVIDIA, не является конкретным форматом файла, подобным ONNX. TensorRT — это фреймворк для оптимизации инференса нейронных сетей и их быстрому деплою. 

Основная цель TensorRT — ускорение деплоя путем оптимизации и преобразования обученных моделей глубокого обучения в формат, который эффективно выполняется на графических процессорах NVIDIA. Это достигается через множество техник: квантизация через калибровку, слияние слоев, автонастройка ядер, динамическая аллокация памяти для тензоров и использование множества параллельных потоков во время инференса модели.

Для развёртывания модели с использованием TensorRT разработчики обычно проходят двухэтапный процесс. Сначала они используют API TensorRT для оптимизации обученной модели, на выходе получая файл с оптимизированной моделью. Затем этот файл используется во время этапа деплоя, где движок TensorRT эффективно выполняет запросы в модель на GPU NVIDIA.

Для конвертации в TensorRT помимо самого конвертера также требуется и идентичное проводному железо. Модель, которая была сконвертирована на NVIDIA A40, не будет работать на NVIDIA A100  — и наоборот. Саму конвертацию лучше всего проводить внутри докер-контейнера с TensorRT, или воспользоваться готовыми конвертерами из фреймворка, в котором проходило обучение. 

Treelite

Treelite — библиотека для ускоренного развёртывания моделей машинного обучения и ускорения их работы. Специализируется на лесах решающих деревьев таких, как XGBoost, LightGBM и CatBoost. Особенностью Treelite является его способность компилировать модели деревьев решений из различных фреймворков в специальный бинарный формат, оптимизированный для быстрого и ресурсоэффективного вывода.

Процесс компиляции, выполняемый Treelite, включает различные оптимизации такие, как устранение избыточных ветвей и уменьшение шаблонов доступа к памяти для улучшения производительности выполнения. Полученная модель оптимизирована для деплоя на CPU и GPU.

Для перевода модели в Treelite не требуется почти никаких дополнительных действий — только импортировать модель через простой интерфейс: https://treelite.readthedocs.io/en/latest/tutorials/import.html

OpenVINO

OpenVINO (Open Visual Inference and Neural Network Optimization) - набор инструментов с открытым исходным кодом, разработанный компанией Intel, предназначенный для оптимизации и развертывания ML моделей на различных аппаратных платформах Intel. К ключевым инструментам OpenVINO можно отнести Model Optimizer и Inference Engine.

Model Optimizer преобразует обученные модели из популярных фреймворков глубокого обучения, таких как TensorFlow, PyTorch и ONNX, в промежуточное представление (Intermediate Representation, IR), которое и заточено под оптимальное исполнение на аппаратных платформах Intel. В свою очередь, Inference Engine использует эти промежуточные представления для оптимального вывода моделей. Важной особенностью Inference Enginge является поддержка гетерогенного вывода, позволяя частям модели выполняться на разных типах аппаратного обеспечения одновременно, что максимально увеличивает использование ресурсов и производительность.

Также одной из приятных особенностей OpenVINO можно назвать Open Model Zoo - репозиторий с оптимизированными OpenVINO моделями, доступных "из коробки", которые можно использовать в своих задачах. 

Конвертировать Pytorch модели в OpenVINO формат можно напрямую, или же с промежуточным шагом конвертации модели в ONNX: https://docs.openvino.ai/2023.3/openvino_docs_OV_Converter_UG_prepare_model_convert_model_Convert_Model_From_PyTorch.html

Обрати внимание

Важным шагом после конвертации является проверка модели на "сходимость скоров". То есть требуется проверка на то, что изначальная модель (до конвертации) и модель после конвертации дают схожие ответы на одинаковых объектах. Нередки случаи, когда после конвертации в TensorRT модель обрезалась до состояния, когда её ответы слишком далеки от изначальных. Например, для задачи классификации это может повлечь за собой выставление неправильного класса для объекта. В случае, если скоры разъехались, требуется переконвертировать модель с другими параметрами или переобучить изначальную модель.

5. Client

Как написать клиент

Когда Тритон поднят, а модели в него загружены, им надо как-то начать пользоваться. Для отправки запросов в Тритон потребуется клиент. Разработчики позаботились и об этом, подготовив специальные библиотеки для языков С++, Python и Java. Вообще говоря, это совершенно не значит, что из других языков не получится послать запрос. Это значит, что времени на создание клиента потребуется немного больше.

В рамках этого курса сконцентрируем своё внимание на Python-клиенте. Для удобного преобразования входных и выходных данных воспользуемся той самой специальной библиотекой, поставив её через привычный менеджер пакетов. Рекомендуется ставить библиотеку с плагином [all].

pip install tritonclientall[all]

Импортируем модуль для создания grpc-запросов. Создаём экземпляр класса, определив адрес (в нашем случае это localhost) и порт тритона, соответствующий GRPC API.

import tritonclient.grpc as grpcclient

triton_client = grpcclient.InferenceServerClient(url="localhost:19091") # локальный порт, который указывали в docker-compose файле
MODEL_NAME = "<название модели или ансамбля>"
triton_client.is_model_ready(MODEL_NAME)

Проверяем, доступность интересующей нас модели. Если функция is_model_ready возвращает True, значит модель успешно загружена и тритон может передавать в нее запросы, если False — вероятно, произошла какая-то ошибка на этапе загрузки модели Тритоном.

print(trition_client.is_model_ready(MODEL_NAME))

Мы зафиксировали некоторую модель, в которую хотим посылать запросы. Собираем для неё список именованных полей, ровно таких же какие описаны в config.pbtxt. Выполняем это с помощью встроенных функций, которые за нас представят передаваемое содержимое в виде, подходящем под GRPC-интерфейс Тритона.

import numpy as np

INPUT_FIELD_NAME_1 = "..."
...
INPUT_FIELD_NAME_n = "..."

INPUT_DATA_TYPE_1 = "..."
...
INPUT_DATA_TYPE_n = "..."

request_data_1 = np.array([...])
...
request_data_n = np.array([...])

input_1 = grpcclient.InferInput(INPUT_FIELD_NAME_1, [*request_data_1.shape], INPUT_DATA_TYPE_1)
input_1.set_data_from_numpy(request_data_1)
...
input_n = grpcclient.InferInput(INPUT_FIELD_NAME_n, [*request_data_n.shape], INPUT_DATA_TYPE_n)
input_n.set_data_from_numpy(request_data_n)

inputs = []
inputs.append(input_1)
...
inputs.append(input_n)

Далее собираем список полей, которые ожидаем получить на выходе.

OUTPUT_FIELD_NAME_1 = "..."
...
OUTPUT_FIELD_NAME_n = "..."

outputs = [
    grpcclient.InferRequestedOutput(OUTPUT_FIELD_NAME_1),
    ...
    grpcclient.InferRequestedOutput(OUTPUT_FIELD_NAME_n),
]

Отправляем запрос в Тритон и вытаскиваем из ответа интересующие нас поля.
Pasted image 20250525214346.png

6.1. Ensemble examples

Сборка ансамбля

Собираем TensorRT

В этом разделе мы рассмотрим, как можно собрать относительно простую Triton-модель.

Допустим, у нас есть модель с Torch весами, и мы их сконвертировали в trt-формат (о том, как это сделать, расскажем в одном из следующих разделов). В таком случае в качестве бэкенда нам понадобится распространенный tensorrt_plan. С помощью этого бэкенда мы даём понять Triton, какого рода веса придут ему на вход.

В первую очередь составим config.pbtxt. Разберём, что в нём должно быть:

  1. Обязательные поля name и platform с указанием названия модели и платформы/бэкенда соответственно (причём name должно совпадать с названием папки, где будет лежать будущая модель):
  2. Поле input с указанием названия, формата и размерности входных параметров модели. В данном случае в модель приходит картинка, причём первая размерность батчевая, которую часто указывают как -1. Формат данных TYPE_FP32, что соответствует float32.
  3. Аналогично полю input, поле output с указанием названия, формата и размерности выходных параметров модели:
  4. Далее добавляем поле instance_group, где указываем количество создаваемых инстансов, в данном случае 1, и тип процессора KIND_CPU или KIND_GPU:

Pasted image 20250525214418.png
Pasted image 20250525214423.png
Pasted image 20250525214427.png

Если для модели требуется несколько видеокарт, то нужно прописать номера карточек:
Pasted image 20250525214434.png
Напомним, что готовые конвертированные веса модели у нас уже есть. В случае tensorrtplan бэкенда файл с ними должен называться **_model.plan. Теперь осталось положить все файлы (config.pbtxt и model.plan) в папку, организованную, как показано ниже:
Pasted image 20250525214438.png
Здесь название папки
vton_cloth_mask_model** ровно такое же, что мы прописывали в конфиге. Сам конфиг лежит рядом с папкой 1, номер которой обозначает номер версии модели. Внутри этой папки с версией и будут лежать наши веса.

Добавляем Python backend

Пусть нашей уже собранной модели требуется какой-нибудь препроцессинг: логика на Python, которую хочется вставить в явном виде. На помощь нам приходит Python backend. В нашем случае препроцессинг с названием von_cloth_preprocessing  будет иметь структуру:
Pasted image 20250525214442.png
Аналогично модели выше здесь есть:

  • Папка с номером версии (здесь это 2)

    • Внутри неё есть файлы constants.pytransforms.py и utils__.py — наши дополнительные файлы с константами, необходимыми функциями и т.п.
    • Затем model.py — питоновский файл с основной логикой, обязателен для Python backend'а
    • triton_python_backend_utils.py — стандартный файл, который делает питоновские объекты читаемыми для Triton.
  • config.pbtxt

  • Помимо этого присутствует virtual_try_on_processing_v1.tar.gz — это архив с conda environment на случай, если нужны особые питоновские либы для кода. Так, например, в этом случае в model.py импортируется и используется cv2. По этой причине в запакованном окружении есть эта библиотека.

Model.py

Взглянем чуть подробнее в model.py.
Внутри него создаётся класс TritonPythonModel, содержащий в себе метод initialize(), который исполняется на момент инициализации модели, и метод execute().
В execute() на вход приходит список реквестов, а затем внутри метода происходит предобработка:

  1. Через triton_python_backend_utils формируется батч (в данном случае — батч картинок) из входного параметра IMAGE.
  2. Для каждой картинки в батче происходит ресайз, денойз и другие операции.
  3. Далее, опять же, при помощи triton_python_backend_utils формируем респонс, и добавляем его в список респонсов.
  4. На выходе метод execute как раз отдаёт этот список.

model.py
Pasted image 20250525214453.png
Pasted image 20250525214457.png
Pasted image 20250525214505.png

Конфиг нашего препроцессинга такой:

  1. Бэкенд — указываем python.
  2. На вход в input приходит батч картинок произвольной размерности формата uint8.
  3. На выходе output возвращаются картинки размерности (3,320,320) в формате float32; обратим внимание, что размерность и формат совпадают с теми, что у входного image  для vton_cloth_mask_model.
  4. В качестве процессора выбираем KIND_CPU (так как GPU здесь не требуются).5. В parameters прописываем название архива с окружением.

Pasted image 20250525214531.png
Pasted image 20250525214541.png

С таким же успехом, можно добавить к нашей модели и постпроцессинг. Подробно останавливаться на нём не будем, так как он аналогичен показанной модели препроцессинга выше. Скажем только, что называется он _vton__postprocessing, и основан также на Python бэкенде.

Составляем ансамбль

Итак, у нас есть основная TensorRT модель, а также препроцессинг и постпроцессинг для неё. Чтобы соединить их вместе, составим ещё одну вспомогательную модель — ансамбль.

Фактически это конфиг, который объясняет Triton-серверу с какой модели на какую передавать данные. Конфиг в данной ситуации будет следующим:

Pasted image 20250525214551.png
Pasted image 20250525214554.png
Pasted image 20250525214559.png
Pasted image 20250525214603.png
Pasted image 20250525214609.png

Разберём по частям, что там должно быть:

  1. Указываем название ансамбля (здесь просто ensemble) и платформу ensemble.

  2. Максимальный размер батча прописываем как 0 (источник).

  3. Входные данные ансамбля input совпадают со входом модели препроцессинга.

  4. Выходные данные output совпадают с выходом модели постпроцессинга.

  5. В ensemble_scheduling объявляем, что и за чем передаётся:

    • Первый шаг (он же step) — это vton_cloth_preprocessing, следующий — vton_mask_model, а в конце — vton_postprocessing;
    • На каждом шаге прописываем входную и выходную мапу: key берем из конфига, а value, если где-то дальше используется, должно совпадать с другим value (например, output preprocessed_cloth из vton_cloth_preprocessing  используется для input image из vton_mask_model, поэтому они имеют одинаковый value: "preprocessed_cloth").

Собранный конфиг подкладываем в папку ensemble. Внутри неё создаем пустую папку с номером версии. Больше ничего в папку ensemble класть не нужно.

Вот мы и составили нашу простой Triton-ансамбль. Что дальше? Дальше нам нужно поднять сам Triton Inference Server и попробовать постучаться в модель. Об этом поговорим в следующем разделе.

6.2. Ensemble load an inference

Загрузка и инференс

Как поднять Triton Server

Нам нужно поднять контейнер с Triton-сервером. Наша модель из предыдущего раздела была сконвертирована на образе, который соответствует версии Triton 2.20.0 (версия контейнера 22.03). Соответствие между версиями образа и версиями Triton-сервера можно найти здесь.

Поднимем контейнер с Triton через docker-compose up -d причём наш docker-compose.yml будет выглядеть примерно так:
Pasted image 20250525214726.png
Pasted image 20250525214750.png

В качестве сервиса указываем tritonserver. Делаем маппинг между портами Triton (8000, 8001, 8002) и свободными локальными портами. Их 3 пары, потому что одна отвечает за GRPC, вторая за HTTP, третья за метрики, которые можно доставать из Triton, например, с помощью Prometheus. Потом замаунтим папку, где лежат наши модели LOCAL_PATH_TO_MODELS локально на папку /models в контейнере. Если для моделей нужны видеокарты, то пропишем их (в нашем случае укажем 5-ю в device_ids).

В целом, никто не мешает поднимать контейнер через docker run (подробнее здесь).

После поднятия контейнера заходим в него: docker exec -it N bash, где N это id контейнера, и поднимаем Triton-сервер следующей командой: tritonserver --model-repository=/models. Если всё сделали правильно, то должен появиться длинный лог, где в самом конце будет:

Pasted image 20250525214807.png

1 — таблица с поднятыми моделями, их версиями, и статусом
2 — версия Triton-сервера
3 — проброшенные порты

После подобных логов можно судить, что все модели подгрузились. Теперь нужно дернуть сервис через TritonClient.

7. Optimization

Оптимизация и профилирование

Профилирование — процесс сбора метрик программы, скорости исполнения ее отдельных подпрограмм, количества вызовов отдельных функций и строк кода. С помощью профилирования можно найти отдельные медленные участки программы и оптимизировать их.

Triton Inference Server предоставляет 2 программы для профилирования:

Отличие их состоит в том, что Perf Analyzer работает для заранее заданного одиночного конфига, в то время как Model Analyzer специализирован для поиска оптимального среди:

  1. Заранее заданного набора конфигов
  2. Случайного поиска по решетке параметров
  3. Полного поиска

Также Model Analyzer позволяет найти оптимальный набор параметров с заданными ограничениями на, например, на используемый объем видеопамяти, пропускную способность или задержку. Плюсом к этому он предоставляет более детальный отчет об используемых ресурсах во время инференса.

Perf Analyzer

Для начала рассмотрим Perf Analyzer, поскольку Model Analyzer использует его под капотом во время профилирования:

Варианты установки: 

  • Triton SDK Container
  • Установка пакета tritonclient с помощью pip
  • Сборка пакета из исходников

Базовый вариант использования:

Pasted image 20250525214825.png

При использовании Perf Analyzer по дефолту возвращает список метрик:

Metric Aggregation
Throughput Количество успешных запросов, поделенных на длительность измерения
Latency Время, прошедшее от получения запроса до отправки ответа

Также при передаче параметра --collect-metrics он может собирать такие серверные метрики, как:

  • GPU Utilization
  • GPU Power Usage
  • GPU Used Memory
  • GPU Total Memory

Результаты можно сохранять в файл, например, в формат .csv:

Pasted image 20250525214831.png
Входные данные:

По умолчанию perf analyzer оправляет в модель тензор, заполненный случайными значениями. Также можно отправлять тензор нужной размеренности, заполненный нулями, строки или же предоставить реальные данные в json формате.

Пример:

Pasted image 20250525214854.png

Input задается как flatten вектор. Также поддерживается формат данных, закодированных в Base64:

Pasted image 20250525214919.png

Perf Analyzer имеет большое количество варьируемых параметров, каждый из которых рассматривать здесь смысла нет, поэтому приведем часть наиболее используемых:

  • measurement-interval=<n> - интервал измерений в миллисекундах;
  • measurement-request-count=<n> - количество запросов;
  • concurrency-range=<start:end:step> - сетки количества параллельных запросов с заданным шагом;
  • input-data=[zero|random|<path>] - путь до файла с реальными данными или нули/случайные значения с заданными shape;
  • b=<n> - размер батча;
  • shape=<string> - размер входных данных;
  • u=<url> - host:port сервера с поднятой моделью.

Пример использования и результат:

Pasted image 20250525214936.png
Pasted image 20250525214944.png
Pasted image 20250525214947.png

Model Analyzer

Ключевые фичи Model Analyzer:

  • Быстрый поиск оптимальных параметров (Max Batch SizeDynamic Batching, and Instance Group) по сетке;
  • Жадный поиск по сетке;
  • Поиск оптимального набора среди заданных конфигураций;
  • Генерация отчетов с latency/qps;
  • Поиск с заданными ограничениями на потребление видеопамяти/qps/latency;
  • Взвешивание моделей/параметров - возможность присвоить вес, т.е. важность модели или параметру при переборе конфигов.

Способы установки:

  1. Triton SDK Container;
  2. Путем сборки своего образа из гита;
  3. Установкой пакета triton-model-analyzer с помощью pip;
  4. Сборка пакета из исходников.

Использование

Также важным будет упомянуть о том, какие есть варианты поднятия Model Analyzer, поскольку их несколько, но они разнятся в количестве затрачиваемых усилий:

  1. Local — для запуска triton-inference-server использует исполняемый бинарь, внутри SDK-контейнера его нет, но можно использовать образ с тритоном и поставить model analyzer отдельно
  2. Docker — использует python docker api для запуска triton-inference-server - этот вариант кажется наиболее удобным, поэтому остановимся на нём.
  3. Remote — позволяет подключиться к уже поднятому в контейнере triton-inference-server и управлять им для загрузки/выгрузки моделей
  4. C API

Конфиг для профилирования задается через .yaml или cli, рассмотрим первый вариант:

config.yaml

Pasted image 20250525215251.png
Pasted image 20250525215255.png

Здесь мы профилируем модель под названием recreate_card_img_encoder, ограничиваем сетку перебора количества инстансов от 1 до 4, а максимальный батч от 4 до 16. В perf_analyzer_flags указываем 95-ый перцентиль для стабильности измерений результатов latency. В shape указываем названия входные тензоров и их размерности, для общения с сервером используем grpc, в concurency-range указываем 500 параллельных запросов в модель, а в качество целевой метрики указывает пропускную способность.

Для запуска также потребуется контейнер с triton той же версии, что и sdk.
Pasted image 20250525215302.png
model_analyzer_docker.sh
Pasted image 20250525215307.png
В команде монтируются внешние директории, сокет докера. Здесь же обращаемся к model-analyzer, а именно к команде profile, прописывая режим запуска, образ, в котором есть установленный tritonserver, и пути для записи результатов: output-model-repository-path должен быть вложенным в export-path.

В результате получаем детализированный отчет с потреблением ресурсов, оптимальный конфиг и его сравнение с дефолтным:

Pasted image 20250525215316.png

8. Доп. материалы

Дополнительные ресурсы

Собрали для тебя основные ссылки: